#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# ======================================================================
#    Copyright (C) 2006 Robert Muth <robert at muth dot org>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 of June 1991.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program in form of the file COPYING;
#    if not, write to the
#
#    Free Software Foundation, Inc. <http://www.fsf.org>
#    59 Temple Place, Suite 330,
#    Boston, MA 02111-1307  USA
# ======================================================================

"""
sound playback and recording component for pytrommler

http://muth.org/Robert/pyTrommler
"""

# python imports
import logging
import sys
import threading
import time

#local imports
import song
import sound

MAX_UNITS_PER_MINUTE = 1000

#======================================================================
class Voice:
    """
    A voice is a wrapper around sample which allows us to play
    back a sample incrementally
    """
    def __init__(self, sample, scale, balance):
        self._sample = sample
        self._scale = scale
        self._balance = balance
        self._current = 0
        return

    def NextSlice(self, length):
        if self._current >= self._sample.Length():
            return None
        slice = self._sample.Slice(self._current, length, self._scale, self._balance)
        assert slice.Length() == length

        self._current += length
        return slice

#======================================================================
class SongPlayer:
    """
    Base class for the playback of voices
    """
    def __init__(self):
        self._voices = []
        return

    def clear(self):
        self._voices = []
        return

    def add_voice(self, sample, volume, balance):
        self._voices.append(Voice(sample, volume, balance))
        return

    def get_next_combined_sample(self, length):
        sum = sound.Sample()
        sum.SetZeros(length)
        if self._voices:
            live_voices = []
            for v in self._voices:
                sample = v.NextSlice(length)
                if sample:
                    sum.Add(sample)
                    live_voices.append(v)
            self._voices = live_voices
        return sum

    def process_beat(self, s, pattern_no, beat_no):
        pattern = s.pattern(pattern_no)
        nsample = 60 * 44100 /  (s.units_per_minute() * pattern.beats_per_unit())
        multiplier = 1.0
        # First pass - handle special drums (name starts with "@"
        for drum_no in xrange(song.MAX_DRUM_NUM):
            if not pattern.beat(drum_no, beat_no): continue
            drum = s.drum(drum_no)
            if drum.mute(): continue
            if not drum.name().startswith("@"): continue
            if drum.name() == "@ACCENT":
                multiplier *= drum.volume()
        for drum_no in xrange(song.MAX_DRUM_NUM):
            if not pattern.beat(drum_no, beat_no): continue
            drum = s.drum(drum_no)
            if drum.mute(): continue
            if drum.name().startswith("@"): continue

            sample = s.drum(drum_no).sample()
            if not sample: continue
            # print "add_voice %d %d %f %f" % (beat_no, drum_no, drum.volume(), drum.panning())
            self.add_voice(sample, multiplier * drum.volume(), drum.panning())
        return nsample

class AudioPlayer(SongPlayer):
    """
    Player via SDL
    """
    def __init__(self):
        SongPlayer.__init__(self)
        self._channel = sound.Channel()
        return

    def play(self, s, pattern_no, beat_no):
        length = self.process_beat(s, pattern_no, beat_no)
        self._channel.PlaySample(self.get_next_combined_sample(length))
        return


class RecordingPlayer(SongPlayer):
    """
    Player into file
    """
    def __init__(self):
        SongPlayer.__init__(self)
        self._fragments = []
        return

    def reset(self):
        self._fragments = []
        self.clear()
        return

    def play(self, s, pattern_no, beat_no):
        length = self.process_beat(s, pattern_no, beat_no)
        self._fragments.append(self.get_next_combined_sample(length))
        return

    def get_wave(self):
        length = 0
        data = []
        for f in self._fragments:
            raw = f.array16bit().tostring()
            assert 4 * f.Length() ==  len(raw)
            length += f.Length()
            data.append(raw)
        prolog =  sound.WaveProlog(length)
        return prolog + "".join(data)


def LoadOneSongDrumSamples(s, i):
    """
    load a drum into a song with the necessary error processing.
    (We try to hide the sample class from the song class.)

    """
    drum = s.drum(i)
    path = drum.path()
    if path == "" or path == "NONE":
        drum.set_sample(None)
        return ""
    try:
        sample = sound.Sample()
        sample.SetFromFile(path)
        drum.set_sample(sample)
        return ""
    except Exception, err:
        return str(err) + " " + path + "\n"


def LoadAllSongDrumSamples(s):
    err = ""
    for i in xrange(song.MAX_DRUM_NUM):
        err += LoadOneSongDrumSamples(s, i)
    return err

def PlayPattern(player, s, pattern_no, progress=0):
    """
    Play a pattern on the given player.
    Used primarily for playing into a file
    """
    pattern = s.pattern(pattern_no)
    logging.info("playing pattern %d [%s]",pattern_no, pattern.name())
    for beat_no in xrange(pattern.num_beats()):
        if progress:
            print "beat %d/%d\r" % (1+beat_no, pattern.num_beats()),
            sys.stdout.flush()
        player.play(s, pattern_no, beat_no)
    return


def PlaySong(player, s, start=-1, end=-1):
    """
    Play a sonf range on the given player.
    Used primarily for playing into a file
    """
    logging.info("units per minute %d ", s.units_per_minute())
    if start < 0: start = 0
    if end < 0: end = s.length() - 1
    assert 0 <= start <= s.length()
    assert 0 <= end <= s.length()
    assert start <= end
    logging.info("playing song from %d to %d", start, end)
    for measure_no in xrange(start, end+1):
        pattern_no = s.measure(measure_no)
        PlayPattern(player, s, pattern_no)
    return
# ======================================================================
class GuiPlayer(threading.Thread):
    """
    NOTE(muth): this is probably not 100% race free
    but good enough for a gui
    """

    modes = ["pattern", "song", "range"]

    def __init__(self, s, player):
        threading.Thread.__init__(self)
        self.setName("PlaybackThread")
        self._start_stop_sem = threading.BoundedSemaphore(1)

        self._player = player
        self._song = s
        self._pattern_no =  0
        self._beat_no = 0
        self._measure_no = 0
        self._range_start = 0
        self._range_end = 0
        self._measure_no = 0
        self._mode = GuiPlayer.modes[0]
        self._loop = 0
        self._callback = None
        # now start the thread which will immediate block on
        # the lock
        self._keep_going = 0 # we set this again to zero in run()
        self._kill = 0
        self.start()
        return

    def kill(self):
        self._kill = 1
        time.sleep(1)
        # if run() is blocking on semaphore make it run again
        self.play()
        return

    def get_pos(self):
        return (self._measure_no, self._beat_no)

    def set_range(self, start, stop):
        self._range_start = start
        self._range_end = stop
        return

    def set_pattern_no(self, no):
        self._pattern_no = no
        return

    def set_loop(self, loop):
        self._loop = loop
        return

    def toggle_loop(self):
        self._loop = not self._loop
        return self._loop

    def set_mode(self, mode):
        assert mode in GuiPlayer.modes
        self._mode = mode
        return

    def toggle_mode(self):
        m = GuiPlayer.modes
        index = (1 + m.index(self._mode)) % len(m)
        self._mode = m[index]
        return self._mode

    def play(self):
        logging.info("play")
        # do nothing when we are already playing
        if self._keep_going:
            return
        self.reset()
        self._start_stop_sem.release()
        return

    def pause(self):
        logging.info("pause")
        self._keep_going = 0
        return

    def toggle_play(self):
        if self._keep_going:
            self.pause()
            return 0
        else:
            self.play()
            return 1

    def reset(self):
        self._beat_no = 0
        self._measure_no = self._range_start
        return

    def set_callback(self, callback):
        self._callback = callback
        return

    def run(self):
        # make sure we start off in paused mode
        self._keep_going = 0
        self._start_stop_sem.acquire()

        while 1:
            if not self._keep_going:
                if self._callback: self._callback(0, 0, 0)
                logging.info("player thread is paused")
                self._start_stop_sem.acquire()
                self._keep_going = 1
                logging.info("player thread playing")

            if self._kill: break

            if self._mode == "pattern":
                pattern = self._song.pattern(self._pattern_no)
                if self._beat_no >= pattern.num_beats():
                    if self._loop:
                        self._beat_no = 0
                    else:
                        self._keep_going = 0
                        continue
                if self._callback: self._callback(0, self._pattern_no, self._beat_no)
                self._player.play(self._song, self._pattern_no, self._beat_no)
                self._beat_no += 1
            else:
                if self._mode == "song":
                    self._range_start = 0
                    self._range_end = self._song.length() - 1
                else:
                    assert self._mode == "range"
                    if not 0 <= self._range_start < self._song.length():
                        self._range_start = 0
                    if not 0 <= self._range_end < self._song.length():
                        self._range_end = self._song.length() - 1

                if not self._range_start <= self._measure_no <= self._range_end:
                    if self._loop:
                        self._measure_no = self._range_start
                        self._beat_no = 0
                    else:
                        self._keep_going = 0
                        continue

                pattern_no = self._song.measure(self._measure_no)
                pattern = self._song.pattern(pattern_no)
                if self._beat_no >= pattern.num_beats():
                    self._measure_no += 1
                    self._beat_no = 0
                    continue

                self._player.play(self._song, pattern_no, self._beat_no)
                if self._callback: self._callback(self._measure_no, pattern_no, self._beat_no)
                self._beat_no += 1
        return
# ======================================================================

if __name__ == '__main__':

    logging.basicConfig(level=logging.INFO,
                        format=("%(levelname)s:"
                                "%(module)s:"
                                "%(funcName)s:"
                                "%(lineno)d:"
                                "%(threadname)s"
                                "%(message)s"))
    s = song.Song()
    audio_player = AudioPlayer()
    filename = sys.argv[1]
    logging.info("loading song file  %s", filename)
    s.deserialize(file(filename).read(), "Test")
    LoadAllSongDrumSamples(s)
    gui_player = GuiPlayer(s, audio_player)
    while 1:
        reply = raw_input("next command>")
        tokens = reply.split()
        print tokens
        if tokens[0] == "q":
            gui_player.kill()
            break
        elif tokens[0] == "p":
            res = gui_player.toggle_play()
            logging.info("player is %s", res)
        elif tokens[0] == "l":
            new_loop = gui_player.toggle_loop()
            logging.info("looping is %d", new_loop)
        elif tokens[0] == "m":
            new_mode = gui_player.toggle_mode()
            logging.info("new mode is %s", new_mode)
    logging.info("done")
    sys.exit(0)
